Skip to main content

Error Propagation

The ? operator:

  • Works with Result and Option.
  • If the value is success (Ok / Some), it unwraps it.
  • If the value is failure (Err / None), it returns early from the current function with that error.

In short:

? = “Try this. If it fails, return the error immediately.”

Why use ??

Without ?, error handling gets verbose:

fn read_number(path: &str) -> Result<i32, std::io::Error> {
let contents = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e),
};

let number = match contents.trim().parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
};

Ok(number)
}
```rust

With `?`, it becomes much cleaner:
```rust
fn read_number(path: &str) -> Result<i32, std::io::Error> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(number)
}

How ? works with Result

Example 1: File reading

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?; // If Err, return Err
let mut contents = String::new();
file.read_to_string(&mut contents)?; // If Err, return Err
Ok(contents)
}

fn main() {
match read_file("hello.txt") {
Ok(text) => println!("File contents:\n{}", text),
Err(e) => println!("Error reading file: {}", e),
}
}
  • If File::open fails → function returns immediately with that error.
  • If read_to_string fails → same.
  • Otherwise, returns Ok(contents).

How ? works with Option

Example 2: Chaining optional values

fn first_even_double(nums: &[i32]) -> Option<i32> {
let first = nums.first()?; // If empty, return None
let even = if first % 2 == 0 {
Some(*first)
} else {
None
}?;
Some(even * 2)
}

fn main() {
let nums = vec![2, 4, 6];
println!("{:?}", first_even_double(&nums)); // Some(4)

let empty: Vec<i32> = vec![];
println!("{:?}", first_even_double(&empty)); // None
}

If any step returns None, the whole function returns None.

Rules for using ?

  1. The function must return a compatible type:
    • Use ? on Result<T, E> → function must return Result<_, E> (or compatible error).
    • Use ? on Option<T> → function must return Option<_>.
  2. Error types must match or be convertible.

Automatic error conversion with From

If your function returns a different error type, Rust uses From to convert automatically.

Example 3: Custom error type

use std::fs;
use std::num::ParseIntError;
use std::io;

#[derive(Debug)]
enum MyError {
Io(io::Error),
Parse(ParseIntError),
}

impl From<io::Error> for MyError {
fn from(err: io::Error) -> MyError {
MyError::Io(err)
}
}

impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> MyError {
MyError::Parse(err)
}
}

fn read_number(path: &str) -> Result<i32, MyError> {
let contents = fs::read_to_string(path)?; // io::Error → MyError
let number = contents.trim().parse::<i32>()?; // ParseIntError → MyError
Ok(number)
}
  • ? automatically converts errors using From.

Example 4: Using ? in main

Rust allows main to return Result:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string("hello.txt")?;
println!("{}", text);
Ok(())
}
  • Any error will automatically be printed and the program exits with a failure code.

? vs unwrap() / expect()

Feature?unwrap() / expect()
On failureReturns error to callerPanics
Use caseRecoverable errorsBugs / impossible cases
Code safetySafer, composableRisky in production

Summary

  • ? propagates errors automatically.
  • Works with Result and Option.
  • Eliminates boilerplate match and if let.
  • Uses From for error type conversion.
  • Encourages writing clean, safe, composable Rust code.